package cn.xuqiudong.elasticsearch.helper;

import cn.xuqiudong.common.base.model.PageInfo;
import cn.xuqiudong.common.util.JsonUtil;
import cn.xuqiudong.elasticsearch.model.EsLookup;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.search.*;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.core.CountRequest;
import org.elasticsearch.client.core.CountResponse;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.index.query.MultiMatchQueryBuilder;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.Scroll;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.FieldSortBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.util.CollectionUtils;

import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;

/**
 *
 *  说明 :  查询相关的接口
 *  @author Vic.xu
 * @since  2020年9月10日下午9:16:01
 */
public class EsSearchHelper {

    private RestHighLevelClient restHighLevelClient;

    public EsSearchHelper() {
    }

    public EsSearchHelper(RestHighLevelClient restHighLevelClient) {
        this.restHighLevelClient = restHighLevelClient;
    }

    /**
     * 分页查询, 高亮显示博文, 最多只能查询10000条
     * 使用from 和size方法
     * 使用scroll的方式不适合前后端交互
     * @param <T>
     * @param lookup
     * @return
     * @throws IOException
     */
    public <T> PageInfo<T> search(EsLookup<T> lookup) throws IOException {
        String index = lookup.getIndexName();
        String keyword = lookup.getKeyword();
        Map<String, BiConsumer<T, String>> highlightFieldAndSetFunctionMap = lookup.getHighlightFieldAndSetFunctionMap();
        if (StringUtils.isBlank(keyword) || CollectionUtils.isEmpty(highlightFieldAndSetFunctionMap)) {
            return PageInfo.instance(0, Collections.emptyList(), lookup);
        }

        if (lookup.getIndex() >= 9999) {
            return PageInfo.instance(10000, Collections.emptyList(), lookup);
        }
        SearchRequest searchRequest = initRequest(index);
        String[] matchQueryFields = highlightFieldAndSetFunctionMap.keySet().stream().toArray(String[]::new);


        QueryBuilder queeryBuilder = initQueeryBuilder(keyword, matchQueryFields);
        SearchSourceBuilder searchSourceBuilder = initSource(queeryBuilder);
        //searchSourceBuilder.query(QueryBuilders.matchAllQuery());
        // 分页
        searchSourceBuilder.from(lookup.getIndex());
        searchSourceBuilder.size(lookup.getSize());
        //高亮字段
        higtlight(searchSourceBuilder, highlightFieldAndSetFunctionMap.keySet());

        //排序
        idSort(searchSourceBuilder);
        searchRequest.source(searchSourceBuilder);

        //  count start
        int count = count(queeryBuilder, index);

        SearchResponse response = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        List<T> list = new ArrayList<T>();
        SearchHits hits = response.getHits();

        /**
         * 此处想修改为通用处理 而不是只能使用固定的类
         * 根据高亮的字段 获取高亮结果  然后调用set方法
         * 几个常用函数接口好像没想到有 入双参，无返回值的
         * 不过翻了下java.util.function包，果然看到个BiConsumer
         * 是符合这种场景的，也省得自定义接口函数了
         *
         */
        hits.forEach(hit -> {
            //结构集构造为对象
            String source = hit.getSourceAsString();
            T model = JsonUtil.jsonToObject(source, lookup.getClazz());
            list.add(model);

            //处理高亮
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();

            if (highlightFields == null || highlightFieldAndSetFunctionMap == null) {
                return;
            }
            //根据传入的高亮字段和 高亮结果的set函数 处理高亮
            highlightFieldAndSetFunctionMap.forEach((field, bc) -> {
                String highlightText = getHighlightText(highlightFields, field);
                if (StringUtils.isNotBlank(highlightText)) {
                    bc.accept(model, highlightText);
                }
            });
        });
        PageInfo<T> pageInfo = PageInfo.instance(count, list, lookup);
        return pageInfo;
    }

    //从highlightFields 获取高亮文本
    private String getHighlightText(@NotNull Map<String, HighlightField> highlightFields, String field) {
        HighlightField highlight = highlightFields.get(field);
        if (highlight == null) {
            return "";
        }
        Text[] fragments = highlight.fragments();
        String text = Arrays.stream(fragments).map(Text::string).collect(Collectors.joining());
        return text;
    }

    /**
     * 初始化SearchRequest
     * @return SearchRequest
     */
    private SearchRequest initRequest(String index) {
        SearchRequest searchRequest = new SearchRequest(index);
        return searchRequest;
    }

    /**
     * 构造 SearchSourceBuilder
     * @param queryBuilder 查询
     * @return
     */
    private SearchSourceBuilder initSource(QueryBuilder queryBuilder) {
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
//        MatchQueryBuilder matchQueryBuilder = matchQuery("content", keyword);
        searchSourceBuilder.query(queryBuilder);
        return searchSourceBuilder;
    }

    /**
     *  根据关键词 和字段构造查询
     * @param keyword 关键词
     * @param matchQueryFields   在哪些字段中查询 
     * @return
     */
    private QueryBuilder initQueeryBuilder(String keyword, String[] matchQueryFields) {
        MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery(keyword, matchQueryFields);
        return multiMatchQueryBuilder;

    }

    /**
     * id 排序
     *
     * @param searchSourceBuilder
     */
    private void idSort(SearchSourceBuilder searchSourceBuilder) {
        FieldSortBuilder idSot = SortBuilders.fieldSort("id").unmappedType("string").order(SortOrder.DESC);
        searchSourceBuilder.sort(idSot);

    }

    /**
     * 高亮显示 <br />
     * <p>
     * 	默认的type Elasticsearch支持三种highlight，默认unified：unified，plain和fvh（fast vector highlighter）
     * 高亮参数中的highlighterType 设置为plain的目的是保证高亮字段里的字段值和原始的值是一致的，而不是仅是原始字段的子集。
     fragmentSize 参数用于控制返回的高亮字段的最大字符数（默认值为 100 ），如果高亮结果的字段长度大于该设置的值，则大于的部分不返回。
     number_of_fragments ,如果 number_of_fragments 值设置为 0，则不会生成片段，而是返回字段的整个内容，当然它会突出显示。如果短文本（例如文档标题或地址）需要高亮显示，但不需要分段，这可能非常方便。
     请注意，在这种情况下会忽略 fragment_size。
     @see https://blog.csdn.net/qq_43077857/article/details/90437410
      * </p>
      * @param searchSourceBuilder
     * @param fields   高亮的字段
     */
    private void higtlight(SearchSourceBuilder searchSourceBuilder, Set<String> fields) {
        //高亮显示
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        fields.forEach(field -> {
            //高亮某个字段
            HighlightBuilder.Field highlightField = new HighlightBuilder.Field(field);
//        	highlightField.highlighterType("unified");
            highlightBuilder.field(highlightField);
        });

        //高亮标签
        highlightBuilder.preTags("<strong style='color:red'>");
        highlightBuilder.postTags("</strong>");
        ////如果要多个字段高亮,这项要为false
        highlightBuilder.requireFieldMatch(false);
        highlightBuilder.fragmentSize(100); //用于控制返回的高亮字段的最大字符数
        highlightBuilder.numOfFragments(3); //显示的高亮的片段数
        highlightBuilder.noMatchSize(150);
        searchSourceBuilder.highlighter(highlightBuilder);
    }

    //获取总页数
    private int count(QueryBuilder queryBuilder, String index) throws IOException {
        CountRequest countRequest = new CountRequest(index);
        // countRequest.source has  deprecated
//        countRequest.source(searchSourceBuilder);
        countRequest.query(queryBuilder);
        CountResponse countResponse = restHighLevelClient.count(countRequest, RequestOptions.DEFAULT);
        long count = countResponse.getCount();
        return (int) count;
    }


    /******************** 以下是ES scroll查询的一些代码***************************************************************/

    protected void scroll() throws IOException {
        final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L));
        SearchRequest searchRequest = initRequest("blog_index");
        searchRequest.scroll(scroll);
        String keyword = "java";
        SearchSourceBuilder searchSourceBuilder = initSource(initQueeryBuilder(keyword, new String[]{"summary", "content"}));

        searchSourceBuilder.size(3);
        searchRequest.source(searchSourceBuilder);

        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        String scrollId = searchResponse.getScrollId();
        SearchHit[] searchHits = searchResponse.getHits().getHits();
        System.out.println(" 第一页 :" + searchHits.length);
        long totalCount = searchResponse.getHits().getTotalHits().value;
        System.out.println("总页数:" + totalCount);
        int page = 1;
        scroll2(scrollId);
        //  while (searchHits != null && searchHits.length > 0) {
        page++;
        /*    System.out.println("scrollId : " + scrollId);
            SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
            scrollRequest.scroll(scroll);
            searchResponse = restHighLevelClient.scroll(scrollRequest, RequestOptions.DEFAULT);
            scrollId = searchResponse.getScrollId();
            searchHits = searchResponse.getHits().getHits();
            System.out.println(" 第"+page+"页:" + searchHits.length);*/
        //}

        ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
        clearScrollRequest.addScrollId(scrollId);
        ClearScrollResponse clearScrollResponse = restHighLevelClient.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
        boolean succeeded = clearScrollResponse.isSucceeded();
        System.out.println(succeeded + "   " + page);
    }

    //    
    protected void scroll2(String scrollId) throws IOException {
        //   String scrollId = "FGluY2x1ZGVfY29udGV4dF91dWlkDnF1ZXJ5VGhlbkZldGNoAxQ2cl8zeTNJQng3eXlFcmZyeldlTQAAAAAAAACdFnAtMnJaZF9IVFZ5RXZ2VGRpRDNaSUEUNjdfM3kzSUJ4N3l5RXJmcnpXZU0AAAAAAAAAnhZwLTJyWmRfSFRWeUV2dlRkaUQzWklBFDdMXzN5M0lCeDd5eUVyZnJ6V2VNAAAAAAAAAJ8WcC0yclpkX0hUVnlFdnZUZGlEM1pJQQ==";
//        SearchRequest searchRequest = initRequest();
        final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L));
        SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
//        String keyword = "java";
//        SearchSourceBuilder searchSourceBuilder = initSource(initQueeryBuilder(keyword, new String[]{"summary", "content"}));
        System.out.println(scrollId);
        scrollRequest.scroll(scroll);
        SearchResponse searchResponse = restHighLevelClient.scroll(scrollRequest, RequestOptions.DEFAULT);
        scrollId = searchResponse.getScrollId();
        System.out.println(scrollId);
        SearchHit[] searchHits = searchResponse.getHits().getHits();
        System.out.println(" size: " + searchHits.length);
    }

}
